using BEPUphysics;
using BEPUphysics.BroadPhaseSystems;
using BEPUphysics.Entities;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using BEPUphysics.CollisionRuleManagement;
using BEPUphysics.MathExtensions;
using System;
using PerformanceUtility.GameDebugTools;
using EventFlowLibrary.LevelComponents;
using System.Collections.Generic;
using EventFlowLibrary;
using ScreenManagement;
using EventFlowLibrary.GameStructure;

namespace LevelComponents
{
    /// <summary>
    /// The type of interactive gameworld camera.
    /// </summary>
    public enum CameraMode
    {
        /// <summary>
        /// First person viewpoint, cannot move the character.
        /// </summary>
        FirstPersonStatic,
        /// <summary>
        /// First person viewpoint, can move the character.
        /// </summary>
        FirstPersonMovement,
        /// <summary>
        /// Non-sprung chase camera, can move the character.
        /// </summary>
        ChaseStiff,
        /// <summary>
        /// Sprung chase camera, can move the character.
        /// </summary>
        ChaseSpring,
        /// <summary>
        /// Offset independent camera, can move the character.
        /// </summary>
        Offset,
        /// <summary>
        /// Manually place the camera position and target.
        /// </summary>
        Manual,
    }

    /// <summary>
    /// Struct defining a camera node in space.
    /// </summary>
    public struct CameraNode
    {
        /// <summary>
        /// Where this camera node is in space.
        /// </summary>
        public Vector3 position;
        /// <summary>
        /// The time of arrival at this node in milliseconds since start of traveling the curve
        /// - in Oscillate mode, both lookat and position curves should end at the same time.
        /// </summary>
        public float time;

        /// <summary>
        /// Create a new node.
        /// </summary>
        /// <param name="_position"></param>
        /// <param name="_time"></param>
        public CameraNode(Vector3 _position, float _time)
        {
            position = _position;
            time = _time;
        }
    }

    /// <summary>
    /// Simple camera class for moving around the demos area.
    /// </summary>
    public class Camera
    {
        /// <summary>
        ///  The world we belong to
        /// </summary>
        public World world;

        /// <summary>
        /// Gets or sets the position of the camera.
        /// </summary>
        public Vector3 Position { get; set; }

        /// <summary>
        /// Gets or sets the yaw rotation of the camera.
        /// </summary>
        public float Yaw
        {
            get { return yaw; }
            set { yaw = MathHelper.WrapAngle(value); }
        }
        private float yaw;

        /// <summary>
        /// Gets or sets the pitch rotation of the camera.
        /// </summary>
        public float Pitch
        {
            get { return pitch; }
            set
            {
                pitch = value;
                if (pitch > MathHelper.PiOver2 * .99f)
                    pitch = MathHelper.PiOver2 * .99f;
                else if (pitch < -MathHelper.PiOver2 * .99f)
                    pitch = -MathHelper.PiOver2 * .99f;
            }
        }
        private float pitch;

        /// <summary>
        /// Gets or sets the speed at which the camera moves.
        /// </summary>
        public float Speed { get; set; }

        /// <summary>
        /// Gets the view matrix of the camera.
        /// </summary>
        public Matrix ViewMatrix { get; private set; }

        /// <summary>
        /// Gets or sets the projection matrix of the camera.
        /// </summary>
        public Matrix ProjectionMatrix { get; set; }

        /// <summary>
        /// Gets the world transformation of the camera.
        /// </summary>
        public Matrix WorldMatrix { get; private set; }

        /// <summary>
        /// How the camera moves and interacts.
        /// </summary>
        public CameraMode Mode { get { return mode; } }
        private CameraMode mode;

        /// <summary>
        /// Camera bob level.
        /// </summary>
        public float CameraBob = 0.0f;
        public float PreviousCameraBob = 0.0f;

        /// <summary>
        /// Whether camera bobbing is enabled.
        /// </summary>
        public bool CameraBobEnabled = true;

        #region Chase Camera Mode

        //The following are used for the chase camera only.
        /// <summary>
        /// Entity to follow around and point at.
        /// </summary>
        private Entity entityToChase;

        /// <summary>
        /// Offset vector from the center of the target chase entity to look at.
        /// </summary>
        private Vector3 offsetFromChaseTarget;

        /// <summary>
        /// Whether or not to transform the offset vector with the rotation of the entity.
        /// </summary>
        private bool transformOffset;

        /// <summary>
        /// Distance away from the target entity to try to maintain.  The distance will be shorter at times if the ray hits an object.
        /// </summary>
        private float distanceToTarget;

        /// <summary>
        /// Current position of the camera for sprung cameras.
        /// </summary>
        Vector3 currentPosition;

        /// <summary>
        /// Hold variable for what we are looking at with chase cameras.
        /// </summary>
        public Vector3 LookAt { get { return lookAt; } }
        private Vector3 lookAt;

        /// <summary>
        /// For sprung cameras, our desired resting position.
        /// </summary>
        private Vector3 desiredPosition;

        /// <summary>
        /// For sprung cameras, how far we desire the camera to be from our lookat target.
        /// </summary>
        private Vector3 desiredPositionOffset;

        /// <summary>
        /// Construct a matrix to transform from object space to worldspace in chase cameras.
        /// </summary>
        Matrix transform;

        /// <summary>
        /// Hold variable for our world matrix backwards.
        /// </summary>
        Vector3 backwards;

        /// <summary>
        /// Hold variable for our camera offset.
        /// </summary>
        Vector3 offset;

        /// <summary>
        /// Find the earliest ray hit that isn't the chase target to position the camera appropriately.
        /// </summary>
        RayCastResult result;

        /// <summary>
        /// The camera's bounding frustum.
        /// </summary>
        //public BoundingFrustum Frustum { get { return frustum; } }
        //private BoundingFrustum frustum;

        /// <summary>
        /// Whether the controller is inverted (1) or not (-1)
        /// </summary>
        public int Inverted;

        /// <summary>
        /// Sets up all the information required by the chase camera.
        /// </summary>
        /// <param name="_mode">Our camera mode.</param>
        /// <param name="target">Target to follow.</param>
        /// <param name="offset">Offset from the center of the entity target to point at.</param>
        /// <param name="transform">Whether or not to transform the offset with the target entity's rotation.</param>
        /// <param name="distance">Distance from the target position to try to maintain.</param>
        public void SetCameraMode(CameraMode _mode, Entity target, Vector3 offset, bool transform, float distance)
        {
            mode = _mode;
            entityToChase = target;
            offsetFromChaseTarget = offset;
            transformOffset = transform;
            distanceToTarget = distance;

            desiredPosition = Vector3.Zero;
            desiredPositionOffset = new Vector3(0.0f, distanceToTarget, distanceToTarget * 3);

            switch (mode)
            {
                case CameraMode.ChaseSpring:
                case CameraMode.ChaseStiff:
                case CameraMode.Offset:
                    currentPosition = Position = entityToChase.Position + entityToChase.OrientationMatrix.Backward * distanceToTarget * 3 + new Vector3(0, distanceToTarget, 0);
                    break;
                case CameraMode.FirstPersonMovement:
                case CameraMode.FirstPersonStatic:
                    Position = new Vector3(entityToChase.BufferedStates.InterpolatedStates.Position.X, entityToChase.CollisionInformation.BoundingBox.Max.Y, entityToChase.BufferedStates.InterpolatedStates.Position.Z);
                    break;
                case CameraMode.Manual:
                    timeSinceStart = 0.0f;
                    break;
            }

            //frustum = new BoundingFrustum(ViewMatrix * ProjectionMatrix);
        }

        #endregion

        #region Camera physics (typically set when creating camera)

        /// <summary>
        /// Physics coefficient which controls the influence of the camera's position
        /// over the spring force. The stiffer the spring, the closer it will stay to
        /// the chased object.
        /// </summary>
        public float Stiffness
        {
            get { return stiffness; }
            set { stiffness = value; }
        }
        private float stiffness =90.0f;

        /// <summary>
        /// Physics coefficient which approximates internal friction of the spring.
        /// Sufficient damping will prevent the spring from oscillating infinitely.
        /// </summary>
        public float Damping
        {
            get { return damping; }
            set { damping = value; }
        }
        private float damping = 34.0f;

        /// <summary>
        /// Mass of the camera body. Heaver objects require stiffer springs with less
        /// damping to move at the same rate as lighter objects.
        /// </summary>
        public float Mass
        {
            get { return mass; }
            set { mass = value; }
        }
        private float mass = 2.83f;

        /// <summary>
        /// Velocity of camera.
        /// </summary>
        public Vector3 Velocity
        {
            get { return velocity; }
        }
        private Vector3 velocity = Vector3.Zero;

        /// <summary>
        /// Hold vector for camera spring stretch.
        /// </summary>
        private Vector3 stretch;

        /// <summary>
        /// Hold vector for camera spring force.
        /// </summary>
        private Vector3 force;

        /// <summary>
        /// Hold vector for camera spring acceleration.
        /// </summary>
        private Vector3 acceleration;

        #endregion

        #region Manual Camera

        /// <summary>
        /// If we are stopping a manual camera at a certain point, track our total desired run time here 
        /// and exit when timeSinceStart > totalRunTime.
        /// TODO
        /// </summary>
        float totalRunTime;

        /// <summary>
        /// Time since the manual camera has been initialized, used for discovering the point on the line.
        /// </summary>
        float timeSinceStart;

        /// <summary>
        /// How many times to loop this camera path.
        /// </summary>
        int totalLoopCount = 1;

        /// <summary>
        /// How many times this path has been looped this time around.
        /// </summary>
        int loopCount = 0;

        /// <summary>
        /// The mode we return to when our path is finished.
        /// </summary>
        CameraMode modeToReturnTo = CameraMode.FirstPersonMovement;

        /// <summary>
        /// 3D curve defining camera position over time.
        /// </summary>
        Curve3D cameraCurvePosition = new Curve3D(CurveLoopType.Oscillate, CurveLoopType.Oscillate);

        /// <summary>
        /// 3D curve defining camera lookat over time.
        /// </summary>
        Curve3D cameraCurveLookat = new Curve3D(CurveLoopType.Oscillate, CurveLoopType.Oscillate);

        public void InitCurves(CurveLoopType _positionPre, CurveLoopType _positionPost, CurveLoopType _lookAtPre, CurveLoopType _lookAtPost,
            List<CameraNode> _positions, List<CameraNode> _lookats)
        {
            // Check oscillation restrictions
            if ((cameraCurveLookat.IsOscillate() || cameraCurvePosition.IsOscillate()) &&
                _positions[_positions.Count - 1].time != _lookats[_lookats.Count - 1].time)
            {
                throw new ArgumentException("In oscillate mode, positions and lookats must match the final time.");
            }

            // Track our time variablesd
            timeSinceStart = 0.0f;
            totalRunTime = MathHelper.Max(_positions[_positions.Count - 1].time, _lookats[_lookats.Count - 1].time);

            // Prepare our curves
            cameraCurvePosition = new Curve3D(_positionPre, _positionPost);
            cameraCurveLookat = new Curve3D(_lookAtPre, _lookAtPost);

            // Add our lists
            for (int i = 0; i < _positions.Count; i++)
            {
                cameraCurvePosition.AddPoint(_positions[i].position, _positions[i].time);
            }
            for (int i = 0; i < _lookats.Count; i++)
            {
                cameraCurveLookat.AddPoint(_lookats[i].position, _lookats[i].time);
            }

            //float time = 0;
            //cameraCurvePosition.AddPoint(new Vector3(-100, 130, -50), time);
            //cameraCurveLookat.AddPoint(new Vector3(0, 100, 0), time);
            //time += 10000;
            //cameraCurvePosition.AddPoint(new Vector3(-175, 10, 153), time);
            //cameraCurveLookat.AddPoint(new Vector3(-174, 10, 0), time);

            // Set tangents for smooth transitions
            cameraCurvePosition.SetTangents();
            cameraCurveLookat.SetTangents();
        }

        #endregion

        /// <summary>
        /// Creates a camera.
        /// </summary>
        /// <param name="pos">Initial position of the camera.</param>
        /// <param name="s">Speed of the camera per second.</param>
        /// <param name="p">Initial pitch angle of the camera.</param>
        /// <param name="y">Initial yaw value of the camera.</param>
        /// <param name="projMatrix">Projection matrix used.</param>
        public Camera(Vector3 pos, float s, float p, float y, Matrix projMatrix, World _world)
        {
            Position = pos;
            Speed = s;
            Yaw = y;
            Pitch = p;
            ProjectionMatrix = projMatrix;

            rayCastFilter = RayCastFilter;

            world = _world;
        }
        //The raycast filter limits the results retrieved from the Space.RayCast while in chase camera mode.
        Func<BroadPhaseEntry, bool> rayCastFilter;
        bool RayCastFilter(BroadPhaseEntry entry)
        {
            return entry != entityToChase.CollisionInformation && (entry.CollisionRules.Personal <= CollisionRule.Normal);
        }

        /// <summary>
        /// Updates the state of the camera.
        /// </summary>
        /// <param name="dt">Time since the last frame in seconds.</param>
        /// <param name="keyboardInput">Input for this frame from the keyboard.</param>
        /// <param name="oldMouseInput">Input from the previous frame from the mouse.</param>
        /// <param name="mouseInput">Input for this frame from the mouse.</param>
        /// <param name="gamePadInput">Input for this frame from the game pad.</param>
#if !WINDOWS
        public void Update(float dt, GamePadState gamePadInput)
        {
            // Only change in these modes
            switch (mode)
            {
                case CameraMode.FirstPersonMovement:
                case CameraMode.FirstPersonStatic:
                case CameraMode.Offset:
                    Yaw += gamePadInput.ThumbSticks.Right.X * -1.5f * dt;
                    Pitch += gamePadInput.ThumbSticks.Right.Y * 1.5f * dt * Inverted;
                    break;
            }

#else
        public void Update(float dt, MouseState mouseInput)
        {
            // Only change in these modes
            switch (mode)
            {
                case CameraMode.FirstPersonMovement:
                case CameraMode.FirstPersonStatic:
                case CameraMode.Offset:
                    Yaw += (200 - mouseInput.X) * dt * .6f;
                    Pitch += (200 - mouseInput.Y) * dt * .6f * Inverted;
                    break;
            }
#endif
            WorldMatrix = Matrix.CreateFromAxisAngle(Vector3.Right, pitch) * Matrix.CreateFromAxisAngle(Vector3.Up, yaw);
            
            // Move camera according to mode
            switch (mode)
            {
                case CameraMode.ChaseSpring:
                case CameraMode.ChaseStiff:
                    // BOTH
                    // Construct a matrix to transform from object space to worldspace
                    transform = Matrix.Identity;
                    transform.Forward = entityToChase.OrientationMatrix.Forward;
                    transform.Up = entityToChase.OrientationMatrix.Up;
                    transform.Right = Vector3.Cross(entityToChase.OrientationMatrix.Up, entityToChase.OrientationMatrix.Forward);

                    if (transformOffset)
                        lookAt = entityToChase.Position + Vector3.TransformNormal(offsetFromChaseTarget, transform);
                    else
                        lookAt = entityToChase.Position;
                    // END BOTH

                    if (mode == CameraMode.ChaseStiff)
                    {
                        Position = entityToChase.Position + entityToChase.OrientationMatrix.Backward * distanceToTarget * 3 + new Vector3(0, distanceToTarget, 0);

                        // Look a little further than our reach (1.1f) to make sure we don't jitter when we bring it close (0.9f)
                        if (entityToChase.Space.RayCast(new Ray(lookAt, Vector3.Normalize(Position - lookAt)), Vector3.Distance(Position, lookAt) * 1.1f, rayCastFilter, out result))
                        {
                            Position = lookAt + (result.HitData.T) * Vector3.Normalize(Position - lookAt) * 0.9f;
                        }
                    }
                    else
                    {
                        // Calculate desired camera properties in world space
                        desiredPosition = entityToChase.Position + Vector3.TransformNormal(desiredPositionOffset, transform);

                        // Calculate spring force      
                        // stretch/force/accel define once  
                        stretch = currentPosition - desiredPosition;
                        force = -stiffness * stretch - damping * velocity;

                        // Apply acceleration
                        acceleration = force / mass;
                        velocity += acceleration * dt;

                        // Apply velocity
                        currentPosition += velocity * dt;

                        // Look a little further than our reach (1.1f) to make sure we don't jitter when we bring it close (0.9f)
                        if (entityToChase.Space.RayCast(new Ray(lookAt, Vector3.Normalize(currentPosition - lookAt)), Vector3.Distance(currentPosition, lookAt) * 1.2f, rayCastFilter, out result))
                        {
                            desiredPosition = currentPosition = lookAt + (result.HitData.T) * Vector3.Normalize(currentPosition - lookAt) * 0.9f;
                        }

                        // Set camera position
                        Position = currentPosition;
                    }

                    // Set view coordinates
                    WorldMatrix = WorldMatrix * Matrix.CreateTranslation(Position);
                    ViewMatrix = Matrix.CreateLookAt(Position, lookAt, entityToChase.OrientationMatrix.Up);
                    break;
                case CameraMode.FirstPersonMovement:
                    //Only move around if the camera has control over its own position, adding camera bob (change here to increase travel distance of bob)
                    if (CameraBobEnabled)
                    {
                        Position = new Vector3(entityToChase.Position.X, entityToChase.CollisionInformation.BoundingBox.Max.Y + ((float)Math.Sin(CameraBob) * 0.35f) - 1.0f, entityToChase.Position.Z);
                    }
                    else
                    {
                        Position = new Vector3(entityToChase.Position.X, entityToChase.CollisionInformation.BoundingBox.Max.Y - 1.0f, entityToChase.Position.Z);
                    }

                    // Set view coordinates
                    WorldMatrix = WorldMatrix * Matrix.CreateTranslation(Position);
                    ViewMatrix = Matrix.Invert(WorldMatrix);
                    break;
                case CameraMode.FirstPersonStatic:
                    Position = new Vector3(entityToChase.Position.X, entityToChase.CollisionInformation.BoundingBox.Max.Y, entityToChase.Position.Z);

                    // Set view coordinates
                    WorldMatrix = WorldMatrix * Matrix.CreateTranslation(Position);
                    ViewMatrix = Matrix.Invert(WorldMatrix);
                    break;
                case CameraMode.Manual:
                    // Set view coordinates
                    // JAMES: this is a hack to end the game
                    if (GlobalVariables.loadVariables.gameState["GameState"] == "EndGame")
                    {
                        timeSinceStart += dt * 1000;

                        // put here to bail out when done if end game - hack
                        if (world.textManager.CurrentEvents.Count == 0 && timeSinceStart >= totalRunTime)
                        {
                            LoadingScreen.Load(world.screenManager, false, world.screenManager.ControllingPlayer, new BackgroundScreen(), new MainMenuScreen(), new CreditsScreen());
                        }

                        // part of the ugly hack to end the game
                        if (timeSinceStart <= totalRunTime)
                        {
                            Position = cameraCurvePosition.GetPointOnCurve(timeSinceStart);
                            lookAt = cameraCurveLookat.GetPointOnCurve(timeSinceStart);
                        }
                    }
                    else
                    {
                        timeSinceStart += dt * 1000;

                        Position = cameraCurvePosition.GetPointOnCurve(timeSinceStart);
                        lookAt = cameraCurveLookat.GetPointOnCurve(timeSinceStart);
                    }
                    
                    WorldMatrix = Matrix.CreateTranslation(Position);
                    ViewMatrix = Matrix.CreateLookAt(Position, lookAt, Vector3.Up); //Matrix.Invert(WorldMatrix);

                    // TODO this may cause jitter in some cases, we should technically check this before the case statement and switch modes then.
                    // TODO ugly hack to end the game
                    if (timeSinceStart >= totalRunTime &&
                        GlobalVariables.loadVariables.gameState["GameState"] != "EndGame")
                    {
                        // Check our loops and whether to change mode
                        loopCount++;
                        if (loopCount == totalLoopCount)
                        {
                            timeSinceStart = loopCount = 0;
                            mode = modeToReturnTo;
                        }
                    }
                    
//#if TRACE
//                    DebugSystem.Instance.EventDisplayer.EnqueueDisplayEvent(Position.ToString());
//#endif
                    break;
                case CameraMode.Offset:
                    if (transformOffset)
                        offset = Matrix3X3.Transform(offsetFromChaseTarget, entityToChase.BufferedStates.InterpolatedStates.OrientationMatrix);
                    else
                        offset = offsetFromChaseTarget;
                    lookAt = entityToChase.BufferedStates.InterpolatedStates.Position + offset;
                    backwards = WorldMatrix.Backward;

                    //Find the earliest ray hit that isn't the chase target to position the camera appropriately.
                    if (entityToChase.Space.RayCast(new Ray(lookAt, backwards), distanceToTarget, rayCastFilter, out result))
                    {
                        Position = lookAt + (result.HitData.T) * backwards * 0.9f; //Put the camera just before any hit spot. James: added 0.9f to keep camera from slipping under steep terrain
                    }
                    else
                    {
                        Position = lookAt + (distanceToTarget) * backwards;
                    }

                    // Set view coordinates
                    WorldMatrix = WorldMatrix * Matrix.CreateTranslation(Position);
                    ViewMatrix = Matrix.Invert(WorldMatrix);
                    break;
            }

            //DebugSystem.Instance.EventDisplayer.EnqueueDisplayEvent(string.Format("Position: {0}, Yaw: {1}, Pitch: {2}", Position, Yaw, Pitch));

            //frustum = new BoundingFrustum(ViewMatrix * ProjectionMatrix);
        }
    }
}